package com.dib.neo.bkfs.fs;


import com.google.common.collect.Lists;

import com.google.common.collect.Sets;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.URI;
import java.nio.channels.FileChannel;
import java.nio.file.*;
import java.nio.file.attribute.FileAttribute;
import java.util.*;

public class BKPath implements Path {

  private final BKFileSystem fs;
  private final FileInfo fileInfo;
  private final byte[] path;

  private volatile int[] offsets;

  BKPath(BKFileSystem fs, byte[] path) {
    this.fs = fs;
    this.path = path;
    fileInfo = new FileInfo(getPath());
    initFileInfo();
  }

  private void initFileInfo() {

    try {
      FileInfo info = fs.bkfs().getFile(getPath());
      fileInfo.syncData(info.getStat(),info.data());
    }catch (IOException e){
      //ignore IOException
    }

  }

  BKPath(BKFileSystem fs, String input) {
    this(fs, input.getBytes());
  }

  public List<BKPath> listPaths(){
    List<BKPath> files = Lists.newArrayList();
    for(String cpath:getChildrenPath()){
      files.add(((BKPath)resolve(cpath)));
    }
    return files;
  }

  public boolean createNewFile() throws IOException{
    return fs.bkfs().touch(getPath(),getFileInfo().getAttributes().getBlockSize());
  }


  public boolean createNewFile(int blockSize) throws IOException{
    return fs.bkfs().touch(getPath(),blockSize);
  }

  public boolean exists(){
    return fs.bkfs().exists(getPath());
  }

  public FileInfo getFileInfo() {
    return fileInfo;
  }

  List<String> getChildren() {
    List<String> names = Lists.newArrayList();
    try {
      for (String name : fs.bkfs().ls(getPath())) {
        names.add(name);
      }
    } catch (IOException e) {
      e.printStackTrace();
    }
    return names;
  }

  List<String> getChildrenPath() {
    List<String> paths = Lists.newArrayList();
    for (String name : getChildren()) {
      StringBuilder path = new StringBuilder(this.getPath());
      path.append("/")
          .append(name);
      paths.add(path.toString());
    }
    return paths;
  }

  BKFileAttributes getFileAttributes() throws IOException {
    return fs.bkfs().getFile(getPath()).getAttributes();
  }

  static BKPath toBKPath(Path obj) {
    if (obj == null) {
      throw new NullPointerException();
    }
    if (!(obj instanceof BKPath)) {
      throw new ProviderMismatchException();
    }
    return (BKPath) obj;
  }

  String getPathForExceptionMessage() {
    return new String(path);
  }

  private void initOffsets() {
    if (offsets == null) {
      int count, index;

      // count names
      count = 0;
      index = 0;
      if (isEmpty()) {
        // empty path has one name
        count = 1;
      } else {
        while (index < path.length) {
          byte c = path[index++];
          if (c != '/') {
            count++;
            while (index < path.length && path[index] != '/') {
              index++;
            }
          }
        }
      }

      // populate offsets
      int[] result = new int[count];
      count = 0;
      index = 0;
      while (index < path.length) {
        byte c = path[index];
        if (c == '/') {
          index++;
        } else {
          result[count++] = index++;
          while (index < path.length && path[index] != '/') {
            index++;
          }
        }
      }
      synchronized (this) {
        if (offsets == null) {
          offsets = result;
        }
      }
    }
  }


  private boolean isEmpty() {
    return path.length == 0;
  }

  // returns an empty path
  private BKPath emptyPath() {
    return new BKPath(fs, new byte[0]);
  }

  @Override
  public BKFileSystem getFileSystem() {
    return fs;
  }


  @Override
  public BKPath getRoot() {
    if (path.length > 0 && path[0] == '/') {
      return getFileSystem().rootDirectory();
    } else {
      return null;
    }
  }

  @Override
  public BKPath getFileName() {
    initOffsets();

    int count = offsets.length;

    // no elements so no name
    if (count == 0) {
      return null;
    }

    // one name element and no root component
    if (count == 1 && path.length > 0 && path[0] != '/') {
      return this;
    }

    int lastOffset = offsets[count - 1];
    int len = path.length - lastOffset;
    byte[] result = new byte[len];
    System.arraycopy(path, lastOffset, result, 0, len);
    return new BKPath(getFileSystem(), result);
  }

  @Override
  public BKPath getParent() {
    initOffsets();

    int count = offsets.length;
    if (count == 0) {
      // no elements so no parent
      return null;
    }
    int len = offsets[count - 1] - 1;
    if (len <= 0) {
      // parent is root only (may be null)
      return getRoot();
    }
    byte[] result = new byte[len];
    System.arraycopy(path, 0, result, 0, len);
    return new BKPath(getFileSystem(), result);
  }

  @Override
  public int getNameCount() {
    initOffsets();
    return offsets.length;
  }

  @Override
  public BKPath getName(int index) {
    initOffsets();
    if (index < 0) {
      throw new IllegalArgumentException();
    }
    if (index >= offsets.length) {
      throw new IllegalArgumentException();
    }

    int begin = offsets[index];
    int len;
    if (index == (offsets.length - 1)) {
      len = path.length - begin;
    } else {
      len = offsets[index + 1] - begin - 1;
    }

    // construct result
    byte[] result = new byte[len];
    System.arraycopy(path, begin, result, 0, len);
    return new BKPath(getFileSystem(), result);
  }

  @Override
  public BKPath subpath(int beginIndex, int endIndex) {
    initOffsets();

    if (beginIndex < 0) {
      throw new IllegalArgumentException();
    }
    if (beginIndex >= offsets.length) {
      throw new IllegalArgumentException();
    }
    if (endIndex > offsets.length) {
      throw new IllegalArgumentException();
    }
    if (beginIndex >= endIndex) {
      throw new IllegalArgumentException();
    }

    // starting offset and length
    int begin = offsets[beginIndex];
    int len;
    if (endIndex == offsets.length) {
      len = path.length - begin;
    } else {
      len = offsets[endIndex] - begin - 1;
    }

    // construct result
    byte[] result = new byte[len];
    System.arraycopy(path, begin, result, 0, len);
    return new BKPath(getFileSystem(), result);
  }


  @Override
  public boolean isAbsolute() {
    return (path.length > 0 && path[0] == '/');
  }

  // Resolve child against given base
  private static byte[] resolve(byte[] base, byte[] child) {
    int baseLength = base.length;
    int childLength = child.length;
    if (childLength == 0) {
      return base;
    }
    if (baseLength == 0 || child[0] == '/') {
      return child;
    }
    byte[] result;
    if (baseLength == 1 && base[0] == '/') {
      result = new byte[childLength + 1];
      result[0] = '/';
      System.arraycopy(child, 0, result, 1, childLength);
    } else {
      result = new byte[baseLength + 1 + childLength];
      System.arraycopy(base, 0, result, 0, baseLength);
      result[base.length] = '/';
      System.arraycopy(child, 0, result, baseLength + 1, childLength);
    }
    return result;
  }

  @Override
  public BKPath resolve(Path obj) {
    byte[] other = toBKPath(obj).path;
    if (other.length > 0 && other[0] == '/') {
      return ((BKPath) obj);
    }
    byte[] result = resolve(path, other);
    return new BKPath(getFileSystem(), result);
  }


  @Override
  public BKPath relativize(Path obj) {
    BKPath other = toBKPath(obj);
    if (other.equals(this)) {
      return emptyPath();
    }

    // can only relativize paths of the same type
    if (this.isAbsolute() != other.isAbsolute()) {
      throw new IllegalArgumentException("'other' is different type of Path");
    }

    // this path is the empty path
    if (this.isEmpty()) {
      return other;
    }

    int bn = this.getNameCount();
    int cn = other.getNameCount();

    // skip matching names
    int n = (bn > cn) ? cn : bn;
    int i = 0;
    while (i < n) {
      if (!this.getName(i).equals(other.getName(i))) {
        break;
      }
      i++;
    }

    int dotdots = bn - i;
    if (i < cn) {
      // remaining name components in other
      BKPath remainder = other.subpath(i, cn);
      if (dotdots == 0) {
        return remainder;
      }

      // other is the empty path
      boolean isOtherEmpty = other.isEmpty();

      // result is a  "../" for each remaining name in base
      // followed by the remaining names in other. If the remainder is
      // the empty path then we don't add the final trailing slash.
      int len = dotdots * 3 + remainder.path.length;
      if (isOtherEmpty) {
        assert remainder.isEmpty();
        len--;
      }
      byte[] result = new byte[len];
      int pos = 0;
      while (dotdots > 0) {
        result[pos++] = (byte) '.';
        result[pos++] = (byte) '.';
        if (isOtherEmpty) {
          if (dotdots > 1) {
            result[pos++] = (byte) '/';
          }
        } else {
          result[pos++] = (byte) '/';
        }
        dotdots--;
      }
      System.arraycopy(remainder.path, 0, result, pos, remainder.path.length);
      return new BKPath(getFileSystem(), result);
    } else {
      // no remaining names in other so result is simply a sequence of ".."
      byte[] result = new byte[dotdots * 3 - 1];
      int pos = 0;
      while (dotdots > 0) {
        result[pos++] = (byte) '.';
        result[pos++] = (byte) '.';
        // no tailing slash at the end
        if (dotdots > 1) {
          result[pos++] = (byte) '/';
        }
        dotdots--;
      }
      return new BKPath(getFileSystem(), result);
    }
  }

  @Override
  public Path normalize() {
    final int count = getNameCount();
    if (count == 0 || isEmpty()) {
      return this;
    }

    boolean[] ignore = new boolean[count];      // true => ignore name
    int[] size = new int[count];                // length of name
    int remaining = count;                      // number of names remaining
    boolean hasDotDot = false;                  // has at least one ..
    boolean isAbsolute = isAbsolute();

    // first pass:
    //   1. compute length of names
    //   2. mark all occurrences of "." to ignore
    //   3. and look for any occurrences of ".."
    for (int i = 0; i < count; i++) {
      int begin = offsets[i];
      int len;
      if (i == (offsets.length - 1)) {
        len = path.length - begin;
      } else {
        len = offsets[i + 1] - begin - 1;
      }
      size[i] = len;

      if (path[begin] == '.') {
        if (len == 1) {
          ignore[i] = true;  // ignore  "."
          remaining--;
        } else {
          if (path[begin + 1] == '.')   // ".." found
          {
            hasDotDot = true;
          }
        }
      }
    }

    // multiple passes to eliminate all occurrences of name/..
    if (hasDotDot) {
      int prevRemaining;
      do {
        prevRemaining = remaining;
        int prevName = -1;
        for (int i = 0; i < count; i++) {
          if (ignore[i]) {
            continue;
          }

          // not a ".."
          if (size[i] != 2) {
            prevName = i;
            continue;
          }

          int begin = offsets[i];
          if (path[begin] != '.' || path[begin + 1] != '.') {
            prevName = i;
            continue;
          }

          // ".." found
          if (prevName >= 0) {
            // name/<ignored>/.. found so mark name and ".." to be
            // ignored
            ignore[prevName] = true;
            ignore[i] = true;
            remaining = remaining - 2;
            prevName = -1;
          } else {
            // Case: /<ignored>/.. so mark ".." as ignored
            if (isAbsolute) {
              boolean hasPrevious = false;
              for (int j = 0; j < i; j++) {
                if (!ignore[j]) {
                  hasPrevious = true;
                  break;
                }
              }
              if (!hasPrevious) {
                // all proceeding names are ignored
                ignore[i] = true;
                remaining--;
              }
            }
          }
        }
      } while (prevRemaining > remaining);
    }

    // no redundant names
    if (remaining == count) {
      return this;
    }

    // corner case - all names removed
    if (remaining == 0) {
      return isAbsolute ? getFileSystem().rootDirectory() : emptyPath();
    }

    // compute length of result
    int len = remaining - 1;
    if (isAbsolute) {
      len++;
    }

    for (int i = 0; i < count; i++) {
      if (!ignore[i]) {
        len += size[i];
      }
    }
    byte[] result = new byte[len];

    // copy names into result
    int pos = 0;
    if (isAbsolute) {
      result[pos++] = '/';
    }
    for (int i = 0; i < count; i++) {
      if (!ignore[i]) {
        System.arraycopy(path, offsets[i], result, pos, size[i]);
        pos += size[i];
        if (--remaining > 0) {
          result[pos++] = '/';
        }
      }
    }
    return new BKPath(getFileSystem(), result);
  }

  @Override
  public boolean startsWith(Path other) {
    if (!(Objects.requireNonNull(other) instanceof BKPath)) {
      return false;
    }
    BKPath that = (BKPath) other;

    // other path is longer
    if (that.path.length > path.length) {
      return false;
    }

    int thisOffsetCount = getNameCount();
    int thatOffsetCount = that.getNameCount();

    // other path has no name elements
    if (thatOffsetCount == 0 && this.isAbsolute()) {
      return that.isEmpty() ? false : true;
    }

    // given path has more elements that this path
    if (thatOffsetCount > thisOffsetCount) {
      return false;
    }

    // same number of elements so must be exact match
    if ((thatOffsetCount == thisOffsetCount) &&
        (path.length != that.path.length)) {
      return false;
    }

    // check offsets of elements match
    for (int i = 0; i < thatOffsetCount; i++) {
      Integer o1 = offsets[i];
      Integer o2 = that.offsets[i];
      if (!o1.equals(o2)) {
        return false;
      }
    }

    // offsets match so need to compare bytes
    int i = 0;
    while (i < that.path.length) {
      if (this.path[i] != that.path[i]) {
        return false;
      }
      i++;
    }

    // final check that match is on name boundary
    if (i < path.length && this.path[i] != '/') {
      return false;
    }

    return true;
  }

  @Override
  public boolean endsWith(Path other) {
    if (!(Objects.requireNonNull(other) instanceof BKPath)) {
      return false;
    }
    BKPath that = (BKPath) other;

    int thisLen = path.length;
    int thatLen = that.path.length;

    // other path is longer
    if (thatLen > thisLen) {
      return false;
    }

    // other path is the empty path
    if (thisLen > 0 && thatLen == 0) {
      return false;
    }

    // other path is absolute so this path must be absolute
    if (that.isAbsolute() && !this.isAbsolute()) {
      return false;
    }

    int thisOffsetCount = getNameCount();
    int thatOffsetCount = that.getNameCount();

    // given path has more elements that this path
    if (thatOffsetCount > thisOffsetCount) {
      return false;
    } else {
      // same number of elements
      if (thatOffsetCount == thisOffsetCount) {
        if (thisOffsetCount == 0) {
          return true;
        }
        int expectedLen = thisLen;
        if (this.isAbsolute() && !that.isAbsolute()) {
          expectedLen--;
        }
        if (thatLen != expectedLen) {
          return false;
        }
      } else {
        // this path has more elements so given path must be relative
        if (that.isAbsolute()) {
          return false;
        }
      }
    }

    // compare bytes
    int thisPos = offsets[thisOffsetCount - thatOffsetCount];
    int thatPos = that.offsets[0];
    if ((thatLen - thatPos) != (thisLen - thisPos)) {
      return false;
    }
    while (thatPos < thatLen) {
      if (this.path[thisPos++] != that.path[thatPos++]) {
        return false;
      }
    }

    return true;
  }

  @Override
  public URI toUri() {
    return fs.toUri(new String(path));
  }

  @Override
  public BKPath toAbsolutePath() {
    if (isAbsolute()) {
      return this;
    }
    // The path is relative so need to resolve against default directory,
    // taking care not to reveal the user.dir
    SecurityManager sm = System.getSecurityManager();
    if (sm != null) {
      sm.checkPropertyAccess("user.dir");
    }
    return new BKPath(getFileSystem(),
        resolve(getFileSystem().defaultDirectory(), path));
  }

  @Override
  public Path toRealPath(LinkOption... options) throws IOException {

    BKPath absolute = toAbsolutePath();
    return absolute;
  }


  @Override
  public WatchKey register(WatchService watcher, WatchEvent.Kind<?>[] events,
      WatchEvent.Modifier... modifiers) throws IOException {
    return null;
  }

  @Override
  public final boolean startsWith(String other) {
    return startsWith(getFileSystem().getPath(other));
  }

  @Override
  public final boolean endsWith(String other) {
    return endsWith(getFileSystem().getPath(other));
  }

  @Override
  public final Path resolve(String other) {
    return resolve(getFileSystem().getPath(other));
  }

  @Override
  public final Path resolveSibling(Path other) {
    if (other == null) {
      throw new NullPointerException();
    }
    Path parent = getParent();
    return (parent == null) ? other : parent.resolve(other);
  }

  @Override
  public final Path resolveSibling(String other) {
    return resolveSibling(getFileSystem().getPath(other));
  }

  @Override
  public final Iterator<Path> iterator() {
    return new Iterator<Path>() {
      private int i = 0;

      @Override
      public boolean hasNext() {
        return (i < getNameCount());
      }

      @Override
      public Path next() {
        if (i < getNameCount()) {
          Path result = getName(i);
          i++;
          return result;
        } else {
          throw new NoSuchElementException();
        }
      }

      @Override
      public void remove() {
        throw new UnsupportedOperationException();
      }
    };
  }

  @Override
  public File toFile() {
    throw new UnsupportedOperationException();
  }

  public BKFile open(Set<? extends OpenOption> options,
      FileAttribute<?>... attrs) throws IOException {
    return (BKFile)FileChannel.open(this,options,attrs);
  }

  public BKFile open(OpenOption... options) throws IOException {
    return (BKFile)FileChannel.open(this,options);
  }


  public InputStream newInputStream() throws IOException {
    Set<OpenOption> options = Sets.newHashSet();
    options.add(StandardOpenOption.READ);
    BKFile clone = this.open(options);
    return new BKFSInputStream(clone);
  }

  public OutputStream newOutputStream(boolean append) throws IOException {
    Set<OpenOption> options = Sets.newHashSet();
    options.add(StandardOpenOption.CREATE);
    options.add(StandardOpenOption.WRITE);
    if(append)options.add(StandardOpenOption.APPEND);
    BKFile clone = this.open(options);
    try {
      if (append) {
        clone.position(clone.size());
      }
      return new BKFSOutputStream(clone);
    }catch (Exception ex){
      ex.printStackTrace();
    }
    return null;
  }

  @Override
  public final WatchKey register(WatchService watcher,
      WatchEvent.Kind<?>... events)
      throws IOException {
    return register(watcher, events, new WatchEvent.Modifier[0]);
  }

  public String getPath() {
    return new String(path);
  }

  @Override
  public String toString() {
    return toUri().toString();
  }



  @Override
  public int compareTo(Path other) {
    int len1 = path.length;
    int len2 = ((BKPath) other).path.length;

    int n = Math.min(len1, len2);
    byte v1[] = path;
    byte v2[] = ((BKPath) other).path;

    int k = 0;
    while (k < n) {
      int c1 = v1[k] & 0xff;
      int c2 = v2[k] & 0xff;
      if (c1 != c2) {
        return c1 - c2;
      }
      k++;
    }
    return len1 - len2;
  }

  FileStore getFileStore() {
    return this.getFileSystem().getFileStore();
  }



}
